IO多路复用模型

文章目录
  1. 1. 前置知识
  2. 2. 为什么需要IO复用
  3. 3. 三种IO复用方法
    1. 3.1. select
    2. 3.2. poll
    3. 3.3. epoll
  4. 4. 三者区别
    1. 4.1. select
    2. 4.2. poll
    3. 4.3. epoll
    4. 4.4. 三者适用场景
  5. 5. 三者原理
    1. 5.1. select
    2. 5.2. poll
    3. 5.3. epoll
  6. 6. 总结
  7. 7. 参考

不同于传统的“一个进程处理一个客户端请求”的方式,IO复用可以让一个进程处理多个客户端的请求,更加节省资源。

前置知识

为什么需要IO复用

一个简单地服务端可能是这样的:

1
2
3
4
5
6
7
8
调用socket()创建套接字
bind()绑定地址和端口
listen()监听套接字
while(1){
调用accept()连接客户端
fork()创建进程B来处理客户端的需求/使用新的线程来执行任务
}
释放资源

当使用上面这种方式来处理客户端的请求时,如果客户端数量特别多,服务端就会创建很多进程或线程来执行任务。这种一个进程/线程对应一个客户端的方式其实是挺浪费资源的,如果让一个进程或线程就能够处理多个客户端的连接,那么就能够减少很多不必要的资源浪费。IO就可以解决这个问题。

三种IO复用方法

select

  • 函数API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    int select (int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    参数:
    maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,因为文件描述符是从0开始的
    readfds检测可读的文件描述符集合
    writefds检测可写的问价描述符集合
    exceptfds检测异常条件出现的文件描述符
    timeout超时时间
    阻塞:设置NULL,会一直阻塞,直到有描述符准备好IO
    立即返回:必须设置timeval结构体,但其中的值为0
    等待一段时间:在规定时间内如果发生IO活动就马上返回,如果一直没有就等超时后再返回。
    返回值:
    返回发生所检测操作的fd总数,错误时返回SOCKET_ERROR。
    发生io活动的fd存储在相应的参数中(会删除所有传入的fd,只留下发生io活动的)
    fd_set
    long类型数组,存储文件描述符。可以用下面几个宏来设置。
    FD_ZERO(fd_set *fdset) 将指定的文件描述符集清空
    FD_SET(fd_set *fdset) 用于在文件描述符集合中增加一个新的文件描述符。
    FD_CLR(fd_set *fdset) 用于在文件描述符集合中删除一个文件描述符。
    FD_ISSET(int fd,fd_set *fdset) 用于测试指定的文件描述符是否在该集合中。
    struct timeval{
    long tv_sec; //seconds
    long tv_usec; //microseconds,时间单位比其他的要更小
    };
  • 使用示例:

poll

  • poll原理跟select基本是一样的,但是fd数量没有了限制。
  • 函数API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
    };

    int poll (struct pollfd *fds, unsigned int nfds, int timeout);
    参数:
    pollfd数组传入要检测的IO活动,和返回发生的IO活动
    nfds表示文件描述符的最大值加1.
    timeout表示超时的毫秒数,负数表示无限阻塞,0表示马上返回。
  • 使用示例:

epoll

  • 函数API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    struct epoll_event {
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
    };
    typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
    } epoll_data_t;

    //创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
    int epoll_create(int size);
    参数:
    size以前是用来作fd数目参考,linux2.6.8之后已经不用了
    返回值:
    如果成功返回一个非负的文件描述符,失败返回-1.

    //epoll描述符的控制接口
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    参数:
    epfd,epoll实例的文件描述符。
    op是请求的操作:
    EPOLL_CTL_ADD
    EPOLL_CTL_MOD
    EPOLL_CTL_DEL
    fd是op操作对应的文件描述符
    event标识要检测的io操作,event中的events按位存储发生的事件信息:
    EPOLLIN:监测读操作。
    EPOLLOUT:写操作。
    EPOLLRDHUP:流socket对端关闭连接或关闭写连接。
    EPOLLPRI:紧急数据可读
    EPOLLERR:关联的文件描述符发生错误
    EPOLLHUP:发生挂断。
    EPOLLET:设置该fd边缘触发模式
    EPOLLONESHOT:用来保证同一SOCKET只能被一个线程处理,不会跨越多个线程。
    返回值:
    成功返回0,错误返回-1并设置errno。

    //来获取发生的IO事件
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    参数:
    epfd,epoll实例的文件描述符。
    events,返回的io操作
    maxevents,要监控的最大文件描述符
    timeout表示超时的毫秒数,负数表示无限阻塞,0表示马上返回。
    返回值:
    返回发生IO事件的fd个数,没有发生返回0,发生错误返回-1.
  • epoll的两种工作模式
    水平触发(LT,Level Trigger):默认的模式,如果发生的IO操作没有被处理,下次仍然会继续提醒。并且同时支持 Blocking 和 No-Blocking。
    边缘触发(ET,Edge Trigger):高速模式,发生的IO操作只提醒一次,如果没有被处理,下次就不再提醒了。效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

  • 使用示例:

三者区别

select

  • 单进程支持最大连接数FD_SETSIZE个,一般32位机器位1024,64位为2048。可以重新编译内核修改数量,但性能无法保证。
  • bitmap存储fd。
  • 将消息从内核空间拷贝到用户空间。
  • 每次调用后都需要对所有的fd(一个描述符对应一个客户端连接)进行遍历,随着fd数量增加,性能下降。
  • 各个平台都有实现,跨平台效果好。
  • 超时精度为纳秒,连接数量少时,实时性较好,适用核反应堆、金融平台等场景。

poll

  • 无数量限制,与select本质上没有区别,用链表存储fd。但连接数较多时无法保证性能。
  • 链表存储fd
  • 将消息从内核空间拷贝到用户空间。(同select)
  • 每次调用后都需要对所有的fd(一个描述符对应一个客户端连接)进行遍历,随着fd数量增加,性能下降。(同select)
  • 只有新一点的系统支持。
  • 超时精度为毫秒。

epoll

  • 连接数很大,1G内存机器可以10万左右连接,2G内存可以20万连接。
  • 传入fd用红黑树存储,发生io操作的fd用双向链表存储。
  • 使用mmap来与内核空间共享内存。
  • 不会由于连接数量增加导致性能过分下降,只有首次调用epoll_ctl拷贝fd,每次调用epoll_wait不拷贝。(由于采用回调函数实现。只有活跃的客户端才会调用回调函数,所以epoll会因为活跃的连接数过多而性能下降)
  • Linux平台专用。
  • 超时精度为毫秒。

三者适用场景

简单地说来,select和poll适合连接数量小、活跃数量多、实时性要求高的情况。而epoll适合客户端的连接数量很大,活跃数量小的情况。

三者原理

select

poll

epoll

总结

参考